package jmine.tec.utils.sql;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;

import jmine.tec.utils.collection.impl.CopyOnWriteHashMap;
import jmine.tec.utils.debug.impl.DebugRuntimeFactory;

/**
 * Wrapper sobre a {@link Connection} que verifica se ha mais de uma {@link Connection} associada a uma {@link Thread}
 * 
 * @author takeshi
 */
public class ConnectionWrapperInvocationHandler extends ProxyOverridingInvocationHandler {
    private final Connection delegate;

    private volatile String allocationStack;

    private volatile boolean closed;

    private static final ThreadLocal<ConnectionWrapperInvocationHandler> TRACKING_CONNECTIONS =
            new ThreadLocal<ConnectionWrapperInvocationHandler>();

    /**
     * C'tor
     * 
     * @param connection {@link Connection}
     */
    public ConnectionWrapperInvocationHandler(Connection connection) {
        super(connection);
        this.delegate = connection;
        ConnectionTracker.getInstance().connectionOpened(this);
        if (DebugRuntimeFactory.ENABLED) {
            this.allocationStack = DebugRuntimeFactory.getCurrentThreadStack();
            if (TRACKING_CONNECTIONS.get() != null) {
                System.err.println("[WARN] current thread " + Thread.currentThread().getName()
                        + " opening a 2nd connection without closing the first!");
                System.err.println(TRACKING_CONNECTIONS.get().allocationStack);
                System.err.println("now allocatint at: \n" + DebugRuntimeFactory.getCurrentThreadStack());
                if (DebugRuntimeFactory.getInstance().getPropertyAsBoolean("connection.overlap.fail")) {
                    throw new IllegalStateException("Overlaping connection allocation for thread: " + Thread.currentThread());
                }
            } else {
                TRACKING_CONNECTIONS.set(this);
            }
        }
    }

    /**
     * Sobrescrevendo de {@link Connection#close()}
     * 
     * @throws SQLException se a connection nao puder ser fechada
     */
    public void close() throws SQLException {
        closed = true;
        delegate.close();
        ConnectionTracker.getInstance().connectionClosed(this);
        TRACKING_CONNECTIONS.remove();
    }

    /**
     * @return the allocationStack
     */
    public String getAllocationStack() {
        if (allocationStack == null) {
            return "";
        }
        return allocationStack;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void finalize() throws Throwable {
        if (!closed) {
            System.err.println("WARNING: CONNECTION LEAKED WITHOUT CLOSING. Allocated at: " + getAllocationStack());
            this.close();
        }
    }

    /**
     * @author takeshi
     */
    public static final class ConnectionTracker {

        private static final ConnectionTracker INSTANCE = new ConnectionTracker();

        /**
         * @return {@link ConnectionTracker}
         */
        public static ConnectionTracker getInstance() {
            return INSTANCE;
        }

        private final Map<ConnectionWrapperInvocationHandler, String> openConnections =
                new CopyOnWriteHashMap<ConnectionWrapperInvocationHandler, String>();

        /**
         * 
         */
        private ConnectionTracker() {
            DebugRuntimeFactory.getInstance().exposeAsJMX("ConnectionTracker", this);
        }

        /**
         * Sinaliza que uma conexao foi aberta
         * 
         * @param conn a {@link Connection}
         */
        public void connectionOpened(ConnectionWrapperInvocationHandler conn) {
            if (DebugRuntimeFactory.ENABLED) {
                openConnections.put(conn, DebugRuntimeFactory.getCurrentThreadStack());
            }
        }

        /**
         * Devolve as conexoes abertas
         * 
         * @return String[]
         */
        public String[] openConnections() {
            return openConnections.values().toArray(new String[0]);
        }

        /**
         * @return int
         */
        public int getNumberOfOpenConnections() {
            return openConnections.size();
        }

        /**
         * Sinaliza que uma conexao foi fechada
         * 
         * @param conn {@link Connection}
         */
        public void connectionClosed(ConnectionWrapperInvocationHandler conn) {
            if (DebugRuntimeFactory.ENABLED) {
                String remove = openConnections.remove(conn);
                if (remove == null) {
                    throw new IllegalStateException("connection: " + conn + " not registered");
                }
            }
        }
    }

}